from commandsniffer import library
from subprocess import run, PIPE
from threading import Thread
from typing import Any, List, Dict, Optional
from enum import Enum

import os
import re
import sys
import json

interceptor_dir = library.__path__[0]
command_sniffer_fifo = '/tmp/command_sniffer_fifo'


class ExecutableType(Enum):
    GCC = 1
    GXX = 2
    CLANG = 3
    CLANGXX = 4
    CLINKER = 5
    OTHER = 6

    @classmethod
    def from_path(cls, exec_path: str) -> 'ExecutableType':
        if os.path.islink(exec_path):
            actual_path = os.readlink(exec_path)
            if not os.path.isabs(actual_path):
                actual_path = os.path.join(os.path.dirname(exec_path), actual_path)
            return cls.from_path(actual_path)
        exec_name = os.path.basename(exec_path)
        if re.match(r'(.*-)*gcc(-\d+)?', exec_name):
            return ExecutableType.GCC
        elif re.match(r'(.*-)*g\+\+(-\d+)?', exec_name):
            return ExecutableType.GXX
        elif re.match(r'(.*-)*clang(-\d+)?', exec_name):
            return ExecutableType.CLANG
        elif re.match(r'(.*-)*clang\+\+(-\d+)?', exec_name):
            return ExecutableType.CLANGXX
        elif re.match(r'(.*-)*ld(-\d+)?', exec_name):
            return ExecutableType.CLINKER
        elif re.match(r'(.*-)*lld(-\d+)?', exec_name):
            return ExecutableType.CLINKER
        elif re.match(r'(.*-)*gold(-\d+)?', exec_name):
            return ExecutableType.CLINKER
        elif re.match(r'(.*-)*ld.gold(-\d+)?', exec_name):
            return ExecutableType.CLINKER
        else:
            return ExecutableType.OTHER


def read_command_desc(desc: str) -> List[Dict[str, Any]]:
    command_db: List[Dict[str, Any]] = []
    current_cmd_desc: Dict[str, Any] = {}
    raw_command: List[str] = []
    desc_lines = desc.split('\n')
    for line in desc_lines:
        line = line.strip()
        if (obj := re.match(r'working_directory: (.*)', line)) is not None:
            if current_cmd_desc:
                current_cmd_desc['command'] = raw_command[0]
                current_cmd_desc['arguments'] = raw_command[1:]
                command_db.append(current_cmd_desc)
                current_cmd_desc = {}
                raw_command = []
            current_cmd_desc['working_directory'] = obj.group(1).strip()
        elif (obj := re.match(r'command:(.*)', line)) is not None:
            raw_command.append(obj.group(1).strip())
        elif (obj := re.match(r'environ:', line)) is not None:
            continue
        elif '=' in line:
            key, value = line.split('=', 1)
            current_cmd_desc.setdefault('environ', {})[key.strip()] = value.strip()
        elif bytes(line, encoding='utf-8') == b'\x00':
            continue
        elif line != '':
            print(f"Warning: Unexpected line in interceptor log: {line}")
    if current_cmd_desc:
        current_cmd_desc['command'] = raw_command[0]
        current_cmd_desc['arguments'] = raw_command[1:]
        command_db.append(current_cmd_desc)
    return command_db


def executable_full_path(command_desc: Dict[str, Any]) -> Optional[str]:
    exec_path = command_desc['command']
    if exec_path.startswith('.'):
        exec_path = os.path.normpath(os.path.join(command_desc['working_directory'], exec_path))
    if os.path.isabs(exec_path):
        return exec_path
    path_enval = command_desc['environ'].get('PATH', '')
    for path in path_enval.split(':'):
        full_path = os.path.join(path, exec_path)
        if os.path.isfile(full_path):
            return full_path
    return None


def check_command_type(command_desc: Dict[str, Any]) -> None:
    exec_path = executable_full_path(command_desc)
    if exec_path is not None:
        exec_type = ExecutableType.from_path(exec_path)
        command_desc['type'] = exec_type.name
    else:
        command_desc['type'] = ExecutableType.OTHER.name


def dump_command_db(command_db: List[Dict[str, Any]], db_dump_path: str) -> None:
    interceptor_clib_build_dir = os.path.join(interceptor_dir, 'build/libsniffer.so')
    for command_desc in command_db:
        if os.path.basename(command_desc['command']) == 'ccache':
            command_desc['command'] = command_desc['arguments'][0]
            command_desc['arguments'] = command_desc['arguments'][1:]
        check_command_type(command_desc)
        old_ld_preload = command_desc['environ'].get('LD_PRELOAD', '').split(' ')
        new_ld_preload = ' '.join(filter(lambda x: x != interceptor_clib_build_dir, old_ld_preload)).strip()
        if not new_ld_preload:
            command_desc['environ'].pop('LD_PRELOAD', '')
        else:
            command_desc['environ']['LD_PRELOAD'] = new_ld_preload
        command_desc['environ'].pop('command_sniffer_fifo', '')
    with open(db_dump_path, 'w') as f:
        json.dump(command_db, f, indent=2)


def do_intercept(output_json_path: str, origin_commands: List[str]) -> None:
    interceptor_clib_build_dir = os.path.join(interceptor_dir, 'build/libsniffer.so')
    build_script_path = os.path.join(interceptor_dir, 'build.sh')
    proc = run(['bash', build_script_path], capture_output=False, stderr=PIPE)
    if proc.returncode != 0:
        print(f'Error building interceptor library: {proc.stderr.decode()}')
        sys.exit(1)
    else:
        print(f"build c lib succeeded")
    env = os.environ.copy()
    if 'LD_PRELOAD' in env:
        env['LD_PRELOAD'] = f'{interceptor_clib_build_dir} {env["LD_PRELOAD"]}'
    else:
        env['LD_PRELOAD'] = interceptor_clib_build_dir

    if os.path.exists(command_sniffer_fifo):
        os.unlink(command_sniffer_fifo)
    os.mkfifo(command_sniffer_fifo)
    env['command_sniffer_fifo'] = command_sniffer_fifo
    command_db: List[Dict[str, Any]] = []

    def reading_from_fifo() -> None:
        nonlocal command_db
        with open(command_sniffer_fifo, 'r') as fifo:
            while True:
                recv = fifo.read().replace(b'\x00'.decode(), '\n').strip()
                if recv == 'SNIFFER_FIFO_CLOSED':
                    break
                if not recv:
                    continue
                command_db += read_command_desc(recv)

    thread = Thread(target=reading_from_fifo)
    thread.start()
    proc = run(origin_commands, stderr=PIPE, env=env)
    with open(command_sniffer_fifo, 'w') as f:
        f.write('SNIFFER_FIFO_CLOSED')
    if proc.returncode != 0:
        print(f'Error running original commands: {proc.stderr.decode()}')
        sys.exit(1)
    thread.join()
    if not command_db:
        print('No intercepted commands found')
        sys.exit(1)
    dump_command_db(command_db, output_json_path)
    print(f'Intercepted commands saved to {os.path.abspath(output_json_path)}')
